package de.learnlib.algorithms.ttt.mealy.it;

import de.learnlib.algorithms.ttt.mealy.it.TTTExperiment.Config;
import de.learnlib.api.algorithm.LearningAlgorithm;
import de.learnlib.api.algorithm.LearningAlgorithm.MealyLearner;
import de.learnlib.api.oracle.MembershipOracle.MealyMembershipOracle;
import de.learnlib.api.query.DefaultQuery;
import de.learnlib.examples.LearningExample.MealyLearningExample;
import de.learnlib.filter.cache.dfa.DFACacheOracle;
import de.learnlib.filter.cache.dfa.DFACaches;
import de.learnlib.filter.cache.mealy.MealyCacheOracle;
import de.learnlib.filter.cache.mealy.MealyCaches;
import de.learnlib.filter.statistic.oracle.DFACounterOracle;
import de.learnlib.filter.statistic.oracle.MealyCounterOracle;
import de.learnlib.oracle.equivalence.*;
import de.learnlib.oracle.membership.SimulatorOracle.MealySimulatorOracle;
import net.automatalib.automata.fsa.DFA;
import net.automatalib.automata.transducers.MealyMachine;
import net.automatalib.util.automata.Automata;
import net.automatalib.words.Alphabet;
import net.automatalib.words.Word;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExperimentRunner {

	public static final String LEARNING_PROFILE_KEY = "Learning";
    public static final String COUNTEREXAMPLE_PROFILE_KEY = "Searching for counterexample";
	
	private static final class RunTest<I,O> implements Callable<Void> {
		private final int testId;
		private final int index;
		private final PrintStream ps;
		private final MealyLearningExample<I,O> example;
		private final LearnerCreator learner;
		private final long seed;
	    
		public RunTest(int testId, int index, long seed, PrintStream ps, MealyLearningExample<I,O> example, LearnerCreator learner) {
			this.testId = testId;
			this.index = index;
			this.ps = ps;
			this.example = example;
			this.learner = learner;
			this.seed = seed;
		}

		@Override
		public Void call() throws Exception {
			for(;;) {
				try {
					System.err.println("Running " + learner.getName() + " test " + testId + " on " + example.toString() + ", ce length = " + index);
					Result res = runTest(example.getAlphabet(), example.getReferenceAutomaton(), learner, index, seed);
					synchronized(ps) {
						ps.println(String.format("%d %d %d %f %d", index, res.totalQueries, res.totalQueriesSymbols, res.ceLength, res.totalRounds));
						ps.flush();
					}
					System.err.println(learner.getName() + " test " + testId + " on " + example.toString() + " finished");
					return null;
				}
				catch(Throwable ex) {
					ex.printStackTrace();
				}
			}
		}
	}
	
	private final Config config;
	
	public ExperimentRunner(Config config) {
		this.config = config;
	}
	
	public void run() throws FileNotFoundException, IOException {
		ExecutorService exec = Executors.newFixedThreadPool(config.numThreads);
		
		File resultDir = new File("results");
		File outputDir = new File(resultDir, config.outputName);
		
		outputDir.mkdirs();
		
		File configFile = new File(outputDir, "config");
		try(PrintStream ps = new PrintStream(configFile)) {
			config.print(ps);
		}
		File outputFile = new File(outputDir, "result.dat");
		PrintStream stream = new PrintStream(outputFile);
		Random random = new Random(config.seed);
		List<Future<?>> futures = new ArrayList<>();
		LearnerCreator[] learners = LearnerCreators.getLearners(config.learners);

		for(int i = 0; i < config.targetSystem.length; i++) {
			RealisticSystem target = new RealisticSystem(config.targetSystem[i], config.fileType);
			long localSeed = random.nextLong();
			for (int k = 0; k < learners.length; k++) {
				LearnerCreator learner = learners[k];
				//PrintStream stream = outputStreams[k];
				RunTest<String,String> rt = new RunTest<>(i, k, localSeed, stream, target, learner);
				Future<?> fut = exec.submit(rt);
				futures.add(fut);
			}


			for (Future<?> f : futures) {
				try {
					f.get();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}
		System.err.println("Closing streams");
		stream.close();
		exec.shutdown();
	}
	
	
	public static <I,O> Result runTest(Alphabet<I> alphabet, MealyMachine<?,I,?,O> model,
                                     LearnerCreator learner, int index, long seed) {
		MealyMembershipOracle<I, O> sul = new MealySimulatorOracle<>(model);
		MealyCounterOracle<I,O> simOracleStats = new MealyCounterOracle<>(sul,"MembershipQuery");

		MealyCacheOracle<I,O> CacheOracle = MealyCaches.createTreeCache(alphabet, simOracleStats);
		//MealyCacheOracle<I,O> effOracle = CacheOracle;
		//notice the parameter of DFAWmethodEQOracle, the second should be 0, the third should be the size of the target system
		//because the size of the target system is unknown, 
		//we set a number and determine to cease the process based on the function Automata.findSeparatingWord
		//MealyWpMethodEQOracle<I,O> wMethod = new MealyWpMethodEQOracle<>(CacheOracle, 0, model.size());
		//MealyRefineWpMethodEQOracle<I,O> wMethod = new MealyRefineWpMethodEQOracle<>(CacheOracle, 0, model.size());
		MealyIncrementalWMethodEQOracle<I,O> wMethod = new MealyIncrementalWMethodEQOracle<>(CacheOracle, alphabet, model.size());
		//MealyRandomWpMethodEQOracle<I,O> wMethod = new MealyRandomWpMethodEQOracle<>(CacheOracle, 0, 10);
		LearningAlgorithm<MealyMachine<?,I,?,O>, I, Word<O>> MealyLearner
			= learner.createLearner(alphabet, CacheOracle);

		MealyLearner.startLearning();

		float ceLengthTotal = 0f;
		long rounds = 0L;
		 while (true) {
             //if(Automata.findSeparatingWord(model, hyp, alphabet) == null )
            //	 break;
             System.out.println("Searching for counterexample");
			 final MealyMachine<?,I,?,O> hyp = MealyLearner.getHypothesisModel();

			 if(hyp.size()==model.size())
				 break;

             DefaultQuery<I, Word<O>> ce = wMethod.findCounterExample(hyp, alphabet);

             if (ce == null) {
                 break;
             }

             System.out.println(ce.getInput().toString());
			 ceLengthTotal = ceLengthTotal + ce.getInput().length();
             // next round ...
             rounds++;
             System.out.println("Starting round " + rounds);
             System.out.println("Learning");

             final boolean refined = MealyLearner.refineHypothesis(ce);
             assert refined;
             System.out.println(simOracleStats.getCount());
             System.out.println(simOracleStats.getSymbolCount());
         }

		MealyMachine<?,I,?,O> res_h = MealyLearner.getHypothesisModel();
		System.out.println("States: " + res_h.size());
        System.out.println("Sigma: " + alphabet.size()); 
		Result res = new Result(learner.getName());
		//Visualization.visualize(res_h, alphabet);
		res.totalQueries = simOracleStats.getCount();
		res.totalQueriesSymbols = simOracleStats.getSymbolCount();
		
		res.totalRounds = rounds;
		if (rounds!=0)
			res.ceLength = ceLengthTotal/rounds;
		else
			res.ceLength = 0;

        if(Automata.findSeparatingWord(model, res_h, alphabet) == null )
        	System.out.println("Learning algorithm is correct");
        else
		{
			System.out.println("Learning algorithm is wrong");
			System.exit(1);
		}
		return res;
	}

}
